Skip to content

feat(pptx/render): applyEdits layout-instantiation + headless render API + font transparency#96

Merged
karthikmudunuri merged 2 commits into
karthikmudunuri/work-on-planfrom
karthikmudunuri/applyedits-layout-instantiation
Jun 12, 2026
Merged

feat(pptx/render): applyEdits layout-instantiation + headless render API + font transparency#96
karthikmudunuri merged 2 commits into
karthikmudunuri/work-on-planfrom
karthikmudunuri/applyedits-layout-instantiation

Conversation

@karthikmudunuri

@karthikmudunuri karthikmudunuri commented Jun 12, 2026

Copy link
Copy Markdown
Member

Stacked on #95 (applyEdits). Base = karthikmudunuri/work-on-plan — review/merge #95 first, then this rebases onto main.

This PR now covers all three remaining release-bundle items: P0 layout-instantiation (incl. non-text placeholders), P1 headless render API, and P2 font transparency.


P0 — layout-instantiation inside applyEdits

Closes the one real lossless gap: applyEdits previously deferred source: { layoutId } to the lossy serializeDeck path. Now a PlannedSlide can clone a source slide ({ slideIndex }) or instantiate a fresh one from the template's own layouts ({ layoutId, fills? }). Because the layout is already a part of source, instantiation is still a lossless patch — the new slide binds to ppt/slideLayouts/<layoutId>.xml (inheriting theme/master/background chrome) while every other part stays byte-identical. That's lossless and scale-with-variety in one deck.

Every placeholder (text and non-text) is materialised as an addressable element with the deterministic id layoutSlotElementId(layoutId, key) (exported), key = the placeholderKey / summarizeLayouts slot key:

  • text / obj<p:sp> + <p:txBody>, populated from fills, editable via setText
  • picture → <p:pic> with a transparent placeholder blip so setImage can repoint it
  • chart / table / other → positioned <p:sp> exposing slot geometry (host fills via addChart / addDiagram)

Geometry is read EMU-native from the layout (falling back to the matching master slot). Unresolvable layoutIdonWarning + skip.

P1 ⭐ — renderDeckToImages (headless visual-QA render)

Browser-free renderer (no Playwright/Chromium/DOM) that draws what the editor draws — native charts via buildChartOption + ECharts SSR, diagrams via layoutDiagram, text/shapes/images/backgrounds in z-order — not the OOXML raster fallbacks.

  • renderDeckToSvg(deck, opts?) → one composed SVG per slide (ECharts loaded on demand, so it never bloats the editor bundle — main chunk stays ~1.1 MB).
  • renderDeckToImages / renderSlideToImage / renderPptxToImages → raster bytes.
  • Rasterisation is an injected hook (opts.rasterizeSvg, e.g. @resvg/resvg-js); the default tries a dynamic resvg import and throws a clear error if it isn't installed — no hard native dep.
  • opts: slides (1-based subset), dpi (canvas scales by dpi/96), format, maxWidth. Deterministic (no animation).

Enables: applyEditsrenderPptxToImages → model inspects → targeted fix → re-render.

P2 — font transparency (deck.fontUsage)

parsePptx now stamps deck.fontUsage: { family, embedded }[] — every font family the text uses, flagged whether the source embeds it (<p:embeddedFontLst> → a real ppt/fonts/* part) or merely references it. Hosts warn at generation time on missing brand fonts. Read-only diagnostic, distinct from deck.fonts (the embeddable payloads).

Note: kept as a new field rather than re-typing deck.fonts, which is load-bearing (the serializer reads its byte payloads to re-embed).


Tests (all new)

  • apply-edits-layout.test.ts — mixed clone + layout-instantiate (3 slides, correct layout rel, chrome inherited, cloned parts byte-identical, zero dangling rels), master-geometry inheritance, unresolved-layout skip.
  • render-deck.test.ts — chart slide carries the real series colour, diagram nodes + image present, 1-based subset, injected rasteriser order, maxWidth cap, no headless-browser import in the source.
  • font-usage.test.ts — embeds font A + references-only font B → fontUsage reports {A:true}, {B:false}.

Verification

  • pnpm typecheck
  • pnpm test ✅ (179 passed, 9 skipped — fixture-gated)
  • pnpm --filter @textcortex/slidewise build:lib ✅ (no warnings; echarts split into its own lazy chunk)

Three changesets + README sections included.

Out of scope (remaining from the earlier bundle, not requested here)

P0b addSlideFromLayout non-text instantiation on the serializeDeck path; F1/F2 image fixes (the plan notes those land in #94).

…ariety)

Support source:{layoutId,fills} in a PlannedSlide — instantiate a fresh slide
from one of the template's OWN layouts inside the lossless byte-patch path. The
layout is already a part of source, so the new slide binds to its
ppt/slideLayouts/<id>.xml (inheriting theme/master/background chrome) while every
other part stays byte-identical.

- Each layout placeholder becomes an addressable, positioned element keyed by
  the deterministic id layoutSlotElementId(layoutId, key) (exported). Text/obj
  slots fill from fills + edit via setText; picture slots get a transparent
  placeholder blip so setImage can repoint them; chart/table/other expose
  geometry for addChart/addDiagram.
- Placeholder geometry is read EMU-native from the layout, falling back to the
  matching master slot — no canvas-px round-trip.
- Unresolvable layoutId → onWarning + skip (never ship a wrong slide).

Threads an instantiated-block map through the edit ops so edits resolve against
the freshly-built slide XML. Tests: mixed clone+instantiate (3 slides, correct
layout rel, chrome inherited, cloned parts byte-identical, zero dangling rels),
master-geometry inheritance, unresolved-layout skip.
…parency

Headless render-to-image for a server-side visual-QA loop. Browser-free (no
Playwright/Chromium/DOM): composes a deterministic SVG per slide that draws what
the editor draws — native charts (buildChartOption + ECharts SSR), diagrams
(layoutDiagram), text/shapes/images/backgrounds in z-order — not the OOXML
raster fallbacks.

- renderDeckToSvg / renderDeckToImages / renderSlideToImage / renderPptxToImages.
- Rasterisation is an injected hook (opts.rasterizeSvg, e.g. @resvg/resvg-js);
  default tries a dynamic resvg import and throws a clear error otherwise — no
  hard native dep. ECharts is loaded on demand so it never bloats the editor
  bundle (main chunk stays ~1.1MB).
- opts: slides (1-based subset), dpi, format, maxWidth.

Font transparency: parsePptx stamps deck.fontUsage {family,embedded}[] — every
family the text uses, flagged embedded (a real ppt/fonts/* part in
<p:embeddedFontLst>) vs only referenced, so the host can warn on missing brand
fonts. Read-only diagnostic, distinct from deck.fonts.

Tests: render-deck (chart colours, diagram, image, subset, injected rasteriser,
maxWidth, browser-free) + font-usage (embed vs reference).
@karthikmudunuri karthikmudunuri changed the title feat(pptx): layout-instantiation in applyEdits (lossless scale-with-variety) feat(pptx/render): applyEdits layout-instantiation + headless render API + font transparency Jun 12, 2026
@karthikmudunuri karthikmudunuri merged commit 0bd2f9c into karthikmudunuri/work-on-plan Jun 12, 2026
1 check passed
karthikmudunuri added a commit that referenced this pull request Jun 12, 2026
…adless render API + font transparency (#98)

* feat(pptx): layout-instantiation in applyEdits (lossless scale-with-variety)

Support source:{layoutId,fills} in a PlannedSlide — instantiate a fresh slide
from one of the template's OWN layouts inside the lossless byte-patch path. The
layout is already a part of source, so the new slide binds to its
ppt/slideLayouts/<id>.xml (inheriting theme/master/background chrome) while every
other part stays byte-identical.

- Each layout placeholder becomes an addressable, positioned element keyed by
  the deterministic id layoutSlotElementId(layoutId, key) (exported). Text/obj
  slots fill from fills + edit via setText; picture slots get a transparent
  placeholder blip so setImage can repoint them; chart/table/other expose
  geometry for addChart/addDiagram.
- Placeholder geometry is read EMU-native from the layout, falling back to the
  matching master slot — no canvas-px round-trip.
- Unresolvable layoutId → onWarning + skip (never ship a wrong slide).

Threads an instantiated-block map through the edit ops so edits resolve against
the freshly-built slide XML. Tests: mixed clone+instantiate (3 slides, correct
layout rel, chrome inherited, cloned parts byte-identical, zero dangling rels),
master-geometry inheritance, unresolved-layout skip.

* feat(render): headless renderDeckToImages + deck.fontUsage font transparency

Headless render-to-image for a server-side visual-QA loop. Browser-free (no
Playwright/Chromium/DOM): composes a deterministic SVG per slide that draws what
the editor draws — native charts (buildChartOption + ECharts SSR), diagrams
(layoutDiagram), text/shapes/images/backgrounds in z-order — not the OOXML
raster fallbacks.

- renderDeckToSvg / renderDeckToImages / renderSlideToImage / renderPptxToImages.
- Rasterisation is an injected hook (opts.rasterizeSvg, e.g. @resvg/resvg-js);
  default tries a dynamic resvg import and throws a clear error otherwise — no
  hard native dep. ECharts is loaded on demand so it never bloats the editor
  bundle (main chunk stays ~1.1MB).
- opts: slides (1-based subset), dpi, format, maxWidth.

Font transparency: parsePptx stamps deck.fontUsage {family,embedded}[] — every
family the text uses, flagged embedded (a real ppt/fonts/* part in
<p:embeddedFontLst>) vs only referenced, so the host can warn on missing brand
fonts. Read-only diagnostic, distinct from deck.fonts.

Tests: render-deck (chart colours, diagram, image, subset, injected rasteriser,
maxWidth, browser-free) + font-usage (embed vs reference).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant